import Dep, { popTarget, pushTarget } from "./dep";

let id = 0;

/* 
    我们可以给模板中的属性 增加一个收集器dep
    页面渲染的时候我们将渲染逻辑封装到watcher中 vm._update(vm._render());
    让dep记住这个watcher即可，稍后属性变化了可以找到对应的dep中存放的watcher进行重新渲染

    > dep会存着渲染逻辑vm._update(vm._render())，当依赖发生变化，重新执行渲染逻辑
    > 观察者模式
       每个属性有一个dep(属性就是被观察者)
       watcher就是观察者（属性变化了会通知观察者来更新）

        需要给每一个属性增加一个dep，目的就是收集watcher
        一个视图（组件）中 有多个属性   n个dep对应一个watcher
        一个属性可以对应多个视图（组件） 1个dep对应多个watcher
        多对多的关系
        变量和watcher本来是没有关联的，可以给变量分配一个dep，让dep把watcher收集起来
*/

// 1) 当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target上
// 2) 调用_render()会取值,会调用Object.defineProperty的get方法
//     ——> 让取值的属性dep记住当前的watcher

// 不同的组件有不同的watcher 目前只有一个，用于渲染根实例
class Watcher {
  constructor(vm, fn, option) {
    this.vm = vm;

    this.id = id++;

    this.renderWatcher = option; // 是一个渲染watcher

    // fn是传进来的渲染方法
    // getter意味着调用这个函数可以发生取值操作
    this.getter = fn;

    // 这里存放着当前watcher对应的属性有哪些
    // > 作用:组件卸载的时候watch清理掉所有相应式数据;计算属性
    this.deps = [];
    this.depsId = new Set(); // 通过set去重

    this.lazy = option.lazy;
    this.dirty = this.lazy; // 缓存值

    // 通过lazy控制是否立即执行
    this.lazy ? undefined : this.get(); //构造的时候默认调一次
  }

  // watcher记住dep
  addDep(dep) {
    // 一个组件对应多个属性,重复的属性也不用记录
    if (!this.depsId.has(dep.id)) {
      this.deps.push(dep);
      this.depsId.add(dep.id);

      // dep记住watcher （重复的也走不到这里）
      dep.addWats(this);
    }
  }

  evaluate() {
    // 获取到用户执行函数的返回值，并且要将标识改为脏
    /* 
      执行get()，执行用户执行函数，会将计算属性的watcher也放入到栈中
      1.初始化计算属性，创建计算属性watcher 2.页面构建创建的渲染watcher 
      > 取计算属性，将计算属性watcher 存入watcher栈中
      > 计算属性的watcher是先创建出来, 但入栈的时候, 是渲染watcher先入，
        因为计算属性默认不先执行，也即不会立即取计算属性
    */
    this.value = this.get();
    this.dirty = false;
  }

  get() {
    // 执行watcher前，把当前的watcher暴露到全局上
    // Dep.target = this; // Dep.targe是静态属性，只有一份
    pushTarget(this);
    let value = this.getter.call(this.vm); //调用渲染方法，用到的属性会去vm上取值
    // Dep.target = null; // 渲染完毕后就清空
    popTarget();

    return value;
  }

  /* 计算属性watcher里面的属性去收集上一层watcher  */
  depend() {
    let i = this.deps.length;
    // 遍历的是计算属性watcher中收集的dep
    // 计算属性watcher里面的属性去收集上一层watcher，也即渲染watcher
    // 收集过程渲染watcher也会收集计算属性中收集dep，因为是双向收集的
    while (i--) {
      this.deps[i].depend();
    }
  }

  update() {
    if (this.lazy) {
      // 如果是计算属性，依赖的值发生变化了，就标识为脏值
      // 计算属性取值的时候就又可以重新取值了
      this.dirty = true;
    } else {
      // this.get(); // 重新渲染
      // 将更新任务延迟，执行最后一次。
      // > 将更新任务放在一个队列中，每次更新都不立即更新，先缓存起来。
      queueWatcher(this); // 将当前的watcher暂存起来
    }
  }

  run() {
    console.log("真正渲染");
    this.get(); // 渲染的时候是用最新的vm来渲染，更新的时候是最终的值
  }
}

// 事件环
// 刷新调度队列
function flushSchedulerQueue() {
  let flushQueue = queue.slice(0); // 拷贝一份

  // 先置空
  queue = [];
  has = {};
  pending = false;

  flushQueue.forEach((q) => {
    q.run();
  });
  // 执行的是拷贝的一份，在刷新的过程中可能有新的watcher，重新放入queue中
}

let queue = [];
let has = {}; //利用对象去重
let pending = false; //防抖标识
function queueWatcher(watcher) {
  const id = watcher.id;
  if (!has[id]) {
    queue.push(watcher);
    has[id] = true;
    // 进到这里，可能还会有多个组件要更新，也不应该每一次都渲染
    // 不管我们的update执行多少次，但是最终只执行一轮刷新操作
    //   >  异步进行批处理
    if (!pending) {
      // 定时器会在主执行栈后执行（同步代码执行后再执行）
      // 第一次进来就开启定时器了，后续就进不来了，因为同一个组件只需要刷新一次
      //   setTimeout(flushSchedulerQueue, 0);
      nextTick(flushSchedulerQueue);
      pending = true;
    }
  }
}

/* 
    let a = 1
    setTimeout(()=>{ log(a) })
    a = 2
    a = 3
    输出a是最后一个，即4
    同理，无论值怎么改，渲染的时候是用最新的vm来渲染，更新的时候是最终的值
*/

let callbacks = [];
let waiting = false;
// 刷新回调队列
function flushCallbacks() {
  let flushCallback = callbacks.slice(0); // 拷贝一份

  // 先置空
  callbacks = [];
  waiting = false;

  flushCallback.forEach((cb) => cb()); // 按照顺序依次执行
}

// 先执行用户的还是先执行内部的？ 谁先调用就谁先执行
// > 也是存入一个队列,让队列里面的任务依次执行 >  异步进行批处理
export function nextTick(cb) {
  // 开启nextTick，将回调放队列里面维护起来
  callbacks.push(cb);
  if (!waiting) {
    // 多次执行合并成一次，“创个标识，开个异步”
    timerFunc(
      flushCallbacks // 同步代码执行完毕，最后一起刷新
    );
    waiting = true;
  }
}

// 不能说nextTick创建了异步任务，而是会将异步任务维护到队列中
// 而内部只是开一个异步来执行
// 所以nextTick是同步还是异步？ 放到队列中是同步，开异步任务则是异步
// nextTick没有直接使用某一个api开开启异步任务，而是采用优雅降级的方法
// > 1.先采用的是Promise（ie不兼容）
//   2.随后会采用MutationObserver（h5的api）
//   3.再而考虑ie专享的setImmediate
//   4.再降级会使用setTimeout
let timerFunc;
if (Promise) {
  timerFunc = () => {
    Promise.resolve().then(flushCallbacks);
  };
} else if (MutationObserver) {
  // 这里传入的函数是异步执行的
  let observer = new MutationObserver(flushCallbacks);
  let textNode = document.createTextNode(1);
  observer.observe((textNode) => {
    CharacterData: true;
  });
  timerFunc = () => {
    textNode.textContent = 2;
  };
} else if (setImmediate) {
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks);
  };
}

export default Watcher;
